package com.matrix.lbs.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.DistanceUnit;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.CompletionSuggestOption;
import co.elastic.clients.elasticsearch.core.search.Suggester;
import co.elastic.clients.elasticsearch.core.search.Suggestion;
import com.alibaba.nacos.common.utils.StringUtils;
import com.matrix.api.lbs.dto.ShopInfoUpdateDTO;
import com.matrix.api.lbs.vo.NearbyShopWithInfoVO;
import com.matrix.api.user.UserApi;
import com.matrix.common.constant.LbsConstants;
import com.matrix.common.domain.PageResult;
import com.matrix.common.enums.ShopStatusEnum;
import com.matrix.lbs.converter.LbsConverter;
import com.matrix.lbs.model.ShopLocationDoc;
import com.matrix.lbs.repository.ShopLocationRepository;
import com.matrix.lbs.service.LbsService;
import lombok.RequiredArgsConstructor;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.client.elc.NativeQueryBuilder;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.suggest.Completion;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author 有点甜
 * @since 2025/7/25
 */
@Service
@RequiredArgsConstructor
public class LbsServiceImpl implements LbsService {

    private final ShopLocationRepository repository;
    private final ElasticsearchOperations elasticsearchOperations;
    private final ElasticsearchClient elasticsearchClient;

    @DubboReference(check = false)
    private UserApi userApi;

    @Override
    public void upsertShopDocument(ShopInfoUpdateDTO dto) {
        ShopLocationDoc doc = new ShopLocationDoc();
        doc.setId(String.valueOf(dto.getShopId()));
        doc.setShopId(dto.getShopId());
        doc.setShopName(dto.getShopName());
        doc.setShopNameSuggest(new Completion(new String[]{dto.getShopName()}));
        doc.setLogoUrl(dto.getLogoUrl());
        doc.setRating(dto.getRating());
        doc.setMonthlySales(dto.getMonthlySales());
        doc.setCreateTime(dto.getCreateTime());
        doc.setStatus(dto.getStatus());
        if (dto.getLatitude() != null && dto.getLongitude() != null) {
            doc.setLocation(new GeoPoint(dto.getLatitude(), dto.getLongitude()));
        }
        doc.setUpdateTime(new Date());
        repository.save(doc);
    }

    @Override
    public List<NearbyShopWithInfoVO> findNearbyShops(double latitude, double longitude, double distanceInKm, String sortType) {
        NativeQueryBuilder queryBuilder = new NativeQueryBuilder();

        queryBuilder.withQuery(q -> q.bool(b -> b
                .filter(f -> f.geoDistance(g -> g.field("location").location(l -> l.latlon(ll -> ll.lat(latitude).lon(longitude))).distance(distanceInKm + "km")))
                .filter(f -> f.term(t -> t.field("status").value(ShopStatusEnum.OPEN.getCode())))
        ));

        buildSort(queryBuilder, sortType, latitude, longitude);
        queryBuilder.withPageable(PageRequest.of(0, 100));
        SearchHits<ShopLocationDoc> searchHits = elasticsearchOperations.search(queryBuilder.build(), ShopLocationDoc.class);
        return searchHits.getSearchHits().stream().map(this::mapSearchHitToNearbyShopVO).collect(Collectors.toList());
    }

    @Override
    public PageResult<NearbyShopWithInfoVO> searchShops(String keyword, int pageNum, int pageSize, Double latitude, Double longitude, String sortType) {
        Pageable pageable = PageRequest.of(pageNum - 1, pageSize);

        NativeQueryBuilder queryBuilder = new NativeQueryBuilder();

        queryBuilder.withQuery(q -> q.bool(b -> {
            b.filter(f -> f.term(t -> t.field("status").value(ShopStatusEnum.OPEN.getCode())));
            if (StringUtils.hasText(keyword)) {
                b.must(m -> m.match(t -> t.field("shopName").query(keyword)));
            }
            return b;
        }));

        queryBuilder.withPageable(pageable);
        buildSort(queryBuilder, sortType, latitude, longitude);
        SearchHits<ShopLocationDoc> searchHits = elasticsearchOperations.search(queryBuilder.build(), ShopLocationDoc.class);

        List<NearbyShopWithInfoVO> vos = searchHits.getSearchHits().stream()
                .map(this::mapSearchHitToNearbyShopVO)
                .collect(Collectors.toList());

        long totalHits = searchHits.getTotalHits();
        long totalPages = (totalHits == 0) ? 0 : (totalHits + pageSize - 1) / pageSize;

        return new PageResult<>(vos, totalHits, totalPages);
    }

    @Override
    public PageResult<NearbyShopWithInfoVO> findRecommendedShops(Long userId, double latitude, double longitude, int pageNum, int pageSize) {
        List<Long> favoriteShopIds = userApi.listFavoritesInternal(userId);

        int querySize = 100;
        NativeQueryBuilder queryBuilder = new NativeQueryBuilder();
        queryBuilder.withQuery(q -> q.bool(b -> b
                .filter(f -> f.geoDistance(g -> g.field("location").location(l -> l.latlon(ll -> ll.lat(latitude).lon(longitude))).distance("10km")))
                .filter(f -> f.term(t -> t.field("status").value(ShopStatusEnum.OPEN.getCode())))
        ));
        queryBuilder.withPageable(PageRequest.of(0, querySize));
        SearchHits<ShopLocationDoc> searchHits = elasticsearchOperations.search(queryBuilder.build(), ShopLocationDoc.class);

        if (searchHits.isEmpty()) {
            return new PageResult<>(Collections.emptyList(), 0, 0);
        }

        Date oneMonthAgo = Date.from(LocalDateTime.now().minusMonths(1).atZone(ZoneId.systemDefault()).toInstant());

        List<NearbyShopWithInfoVO> sortedShops = searchHits.getSearchHits().stream()
                .map(hit -> {
                    ShopLocationDoc doc = hit.getContent();
                    NearbyShopWithInfoVO vo = LbsConverter.INSTANCE.toNearbyShopWithInfoVO(doc);

                    // 计算推荐分
                    double ratingScore = (doc.getRating() != null ? doc.getRating().doubleValue() : 3.0) * 50.0;
                    double salesScore = Math.log10(doc.getMonthlySales() + 1) * 30.0;
                    double favoriteBonus = favoriteShopIds.contains(doc.getShopId()) ? 100.0 : 0.0;
                    double newShopBonus = (doc.getCreateTime() != null && doc.getCreateTime().after(oneMonthAgo)) ? 20.0 : 0.0; // createTime需要加入ES

                    double totalScore = ratingScore + salesScore + favoriteBonus + newShopBonus;
                    vo.setRecommendationScore(totalScore);

                    // 设置距离
                    List<Object> sortValues = hit.getSortValues();
                    if (!sortValues.isEmpty() && sortValues.get(0) instanceof Double) {
                        vo.setDistance(Math.round((Double) sortValues.get(0) * 100.0) / 100.0);
                    }
                    return vo;
                })
                .sorted(Comparator.comparing(NearbyShopWithInfoVO::getRecommendationScore).reversed())
                .collect(Collectors.toList());

        long totalElements = sortedShops.size();
        int start = (pageNum - 1) * pageSize;
        int end = Math.min(start + pageSize, (int)totalElements);

        if (start >= end) {
            return new PageResult<>(Collections.emptyList(), totalElements, (totalElements + pageSize - 1) / pageSize);
        }

        List<NearbyShopWithInfoVO> paginatedList = sortedShops.subList(start, end);

        return new PageResult<>(paginatedList, totalElements, (totalElements + pageSize - 1) / pageSize);
    }

    @Override
    public List<String> getSearchSuggestions(String prefix) {
        if (!StringUtils.hasText(prefix)) {
            return Collections.emptyList();
        }

        String suggestName = "shop-name-suggester";
        String indexName = "matrix_shops";

        Suggester suggester = Suggester.of(s -> s
                .suggesters(suggestName, fs -> fs
                        .prefix(prefix)
                        .completion(cs -> cs
                                .field("shopNameSuggest")
                                .skipDuplicates(true)
                                .size(10)
                                .fuzzy(f -> f.fuzziness("AUTO"))
                        )
                )
        );

        SearchRequest searchRequest = SearchRequest.of(s -> s
                .index(indexName)
                .suggest(suggester)
                .size(0)
        );

        try {
            SearchResponse<ShopLocationDoc> response = elasticsearchClient.search(searchRequest, ShopLocationDoc.class);

            Map<String, List<Suggestion<ShopLocationDoc>>> suggestMap = response.suggest();
            if (suggestMap == null || !suggestMap.containsKey(suggestName)) {
                return Collections.emptyList();
            }

            List<Suggestion<ShopLocationDoc>> suggestions = suggestMap.get(suggestName);
            if (suggestions == null || suggestions.isEmpty()) {
                return Collections.emptyList();
            }

            List<String> result = new ArrayList<>();
            for (Suggestion<ShopLocationDoc> suggestion : suggestions) {
                if (suggestion.completion() != null && suggestion.completion().options() != null) {
                    suggestion.completion().options().stream()
                            .map(CompletionSuggestOption::text)
                            .forEach(result::add);
                }
            }
            return result.stream().distinct().collect(Collectors.toList());

        } catch (IOException e) {
            throw new RuntimeException("Elasticsearch suggestion query failed", e);
        }
    }

    private void buildSort(NativeQueryBuilder queryBuilder, String sortType, Double latitude, Double longitude) {
        switch (sortType.toLowerCase()) {
            case LbsConstants.SORT_TYPE_DISTANCE:
                if (latitude != null && longitude != null) {
                    queryBuilder.withSort(s -> s.geoDistance(g -> g
                            .field("location")
                            .location(l -> l.latlon(ll -> ll.lat(latitude).lon(longitude)))
                            .order(SortOrder.Asc)
                            .unit(DistanceUnit.Kilometers)
                    ));
                }
                break;
            case LbsConstants.SORT_TYPE_RATING:
                queryBuilder.withSort(s -> s.field(f -> f.field("rating").order(SortOrder.Desc)));
                break;
            case LbsConstants.SORT_TYPE_DEFAULT:
            default:
                if (latitude != null && longitude != null) {
                    queryBuilder.withSort(s -> s.geoDistance(g -> g
                            .field("location")
                            .location(l -> l.latlon(ll -> ll.lat(latitude).lon(longitude)))
                            .order(SortOrder.Asc)
                            .unit(DistanceUnit.Kilometers)
                    ));
                } else {
                    queryBuilder.withSort(s -> s.field(f -> f.field("rating").order(SortOrder.Desc)));
                }
                break;
        }
        queryBuilder.withSort(s -> s.field(f -> f.field("shopId").order(SortOrder.Asc)));
    }

    private NearbyShopWithInfoVO mapSearchHitToNearbyShopVO(SearchHit<ShopLocationDoc> hit) {
        ShopLocationDoc doc = hit.getContent();

        NearbyShopWithInfoVO vo = LbsConverter.INSTANCE.toNearbyShopWithInfoVO(doc);

        List<Object> sortValues = hit.getSortValues();
        if (!sortValues.isEmpty()) {
            if (sortValues.get(0) instanceof Double) {
                double distance = (Double) sortValues.get(0);
                vo.setDistance(Math.round(distance * 100.0) / 100.0);
            }
        }
        return vo;
    }
}
